umijs + qiankun
This document records the issues encountered and the solutions adopted when the site administrator carried out a technical upgrade for a medical company in Changsha, using the umijs + antd + qiankun technology stack.
🚀概述
Umi, pronounced as "Wu Mi" in Chinese, is an extendable enterprise-level front-end application framework. Umi is based on routing, supporting both configured routing and conventional routing, ensuring the completeness of routing functions and expanding based on this. It is equipped with a life cycle comprehensive plugin system, covering every life cycle from source code to build products, supporting various functional extensions and business needs. 🐳
Static configuration is in config/config.ts, and runtime configuration is in src/app.tsx. Dependencies are injected correspondingly when plugins are enabled in config.ts. For example:
// 开启qiankun依赖
{
qiankun: {
master: {},
}
};
Runtime access to qiankun configuration
/**
* 乾坤配置 加载微应用数据
* @returns
*/
export function qiankun() {
const config: QiankunConfigType & { master: { prefetch: boolean } } = {
master: {
apps: [],
prefetch: false,//这里实际是有用的
},
}
fetchAppData().then((res) => {
if (res.data) {
const microData = res.data;
microData.map((item: AppAPI.AppItemType) => {
config.master.apps.push({
name: item.number,
entry: item.url || '',
})
})
}
})
return config;
}
Parent-child communication,if both parent and child applications use umijs, it is relatively simple to perform data interaction and responsive processing according to the official website. However, this micro application is written in vue, and considering that subsequent applications will have various architectures, a more general approach is adopted, and some special processing is carried out due to business requirements.
/**
* const { micro,kp } = useModel('@@qiankunStateForSlave');
* 父子应用通信 该函数的返回值将传递 给子应用
* @returns
*/
export function useQiankunStateForSlave() {
//API函数
const [api, setApi] = useState({
...microUtil
})
//响应式API
const [respApi, setRespApi] = useState({})
//响应式数据
const [state, doSetState]: UseStateType<MicroGlobalStateType> = useState({
callback: {}
})
//回调函数
kpSetCallback({ useEffect, state });
// 实际给子应用调用修改state的方法
// 传参和实现可以自定义,子应用直接调用setState是不生效的,所以才需要这个额外的方法,这是一个坑
const setState = (state: any) => {
doSetState(state)
}
return {
micro: { api, respApi, state, setState },
kp: { setApi, setRespApi, doSetState }
}
}
When loading the child application, the <MicroApp/>
component provided by umijs is used, compared with<MicroAppWithMemoHistory/>
, there is no need to specify the URL, which is related to a subsequent pitfall.
The code is too much, only the key code is posted here
const element = React.useMemo(() => {
let cachedElement = getCachedElement(url);
if (cachedElement) {
return cachedElement;
}
cachedElement = <MicroApp
settings={settings}
wrapperClassName={cns('microContainer')}
name={name}
url={url}
autoCaptureError
autoSetLoading
loader={loaderHadler}
errorBoundary={ErrorComponent}
{...microData}
loadingState
base={base}
pageContext={pageContext}
/>
saveElement(url, cachedElement);
return cachedElement;
}, [name, url, microData.data.userLayout.component]);
🚀Start Pitfall
Pitfall 1 Child application not found
The data of the child application is as follows
const microList = [
{
name: 'app1',
entry: '//localhost:5556',
},
];
Situation one: Do not enable proxy, looking for problems can go crazy. Tracking to the bottom, you will find that the loaded address is the address before proxying. The cross-domain issue has been processed by the component itself, and we don't need to consider it, although the front end may report a cross-domain error, but the actual loading address is wrong.
Situation two: Application died in status LOADING_SOURCE_CODE: You need to export the functional lifecycles in xxx entry.
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('react app bootstraped');
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container ? props.container.querySelector('#root') : document.getElementById('root'),
);
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props);
}
If you can't find these functions, it may be due to the webpack configuration of the child application
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`,
// chunkLoadingGlobal: `webpackJsonp_${name}`,
// globalObject: 'window',
}
Other reasons can be seen on the official website: qiankun common questions
Pitfall 2 Parent-child communication, data needs to be distinguished as local/global, global data is all responsive data, responsive data needs to consider how to provide corresponding callback processing, or how to notify the child application of data changes
//qiankun fun state
const { kp } = useModel('@@qiankunStateForSlave');
//set函数
const { doSetState, setRespApi } = kp;
//简单数据
const microData = kpGetMicroData(initialState!, name, component);
//响应式函数
const respApi = kpGetMicroRespApi(setInitialState);
useEffect(() => {
//追加响应式数据
kpSetMicroState({ doSetState, initialState, antdToken });
//追加响应式函数
setRespApi((pre: any) => ({
...pre,
...respApi,
}))
}, [initialState!.currentUser!, antdToken])
In the child application, I used the callback method
/**
* 加载
*/
export async function mount(props) {
//1.存储props为全局方便其他地方使用
await store.dispatch('micro/setMicroProps', props);
//事件
const onSettingsChange = (settings) => {
console.log('onSettingsChange settings', settings)
}
const onCurrentUserChange = (currentUser) => {
console.log('onCurrentUserChange currentUser', currentUser)
}
const onAntdTokenChange = (antdToken) => {
console.log('onAntdTokenChange antdToken', antdToken)
}
//2.设置回调函数
props.micro.setState((pre) => ({
...pre,
callback: {
onSettingsChange,
onCurrentUserChange,
onAntdTokenChange,
}
}))
//3.渲染目标页面
render(props);
//4.关闭加载动画
props.setLoading(false)
}
Pitfall 3 Child application route interception leads to infinite loop loading
Applications generally have route interception to determine whether the user is logged in and whether the access path is legal. Since the path passed by the main application belongs to the main application's routing rules, the child application's routing rules may not be consistent with the main application's, nor should they be. For example, I have two applications, both of which have a route called /home
. This cannot be distinguished in the main application. My approach is to add a prefix in front of each application to indicate the application, such as: /app1/home
.
At this time, the child application should not have routing because the main application's routing can already tell the child application which page to load, and it can load directly.
function render(props = {}) {
if (window.__POWERED_BY_QIANKUN__ && props.data.userLayout.component) {
const component = props.data.userLayout.component;
let path = component.replace('@/', './');
import(path + '/index.vue').then((module) => {
const container = module.default;
instance = createApp(MicroLayout);
instance
.use(store)
.use(Antd)
.use(Print)
.mount(props.container ? props.container.querySelector('#root') : '#root');
}).catch((error) => {
console.error('Failed to load component:', error);
});
} else {
instance = createApp(App);
instance
.use(store)
.use(router)
.use(Antd)
.use(Print)
.mount(props.container ? props.container.querySelector('#root') : '#root');
}
}
Pitfall 4 The child application uses antd, and the main application also uses antd, resulting in the loss of the child application's popup style
At first, it was thought to be a style conflict issue. A unified prefix was configured in the main application's config.ts
antd: {
//设置class前缀 避免和微应用冲突
configProvider:{
prefixCls: 'kp-ant',
iconPrefixCls: 'kp-anticon',
}
}
In fact, this was not the reason, but this step is still necessary to avoid class conflicts of antd components.
It must be mentioned that antd's popup will append elements to the bottom of the outermost layer, directly outside the qiankun wrapper, which is the reason. The solution is to specify the container for mounting:
<a-drawer :get-container="$el"
:title="title"
:width="720"
:visible="visible"
:body-style="{ paddingBottom: '80px' }"
@close="onClose"
>...<a-drawer/>
Different components have different ways of writing, which can be seen in the official documentation of the components.
🚀Conclusion
The above are the issues that the author is more impressed with, and most of these issues cannot be solved by facing Baidu during the author's processing process. As for other issues, they can generally be found on Baidu. All the writing methods or solutions mentioned above are personally explored by the author and do not represent authoritative solutions, and are for reference only.